# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

.SUFFIXES:

GEN_DIR             := $(realpath ../../../vendor/google_riscv-dv)
TOOLCHAIN           := ${RISCV_TOOLCHAIN}
export IBEX_ROOT    := $(realpath ../../../)
EXT_DIR             := riscv_dv_extension

# Explicitly ask for the bash shell
SHELL                := bash

# Seed for instruction generator and RTL simulation
#
# By default, SEED is set to a different value on each run by picking a random
# value in the Makefile. For overnight testing, a sensible seed might be
# something like the output of "date +%y%m%d". For regression testing, you'll
# need to make sure that a the seed for a failed test "sticks" (so we don't
# start passing again without fixing the bug).
SEED                := $(shell echo $$RANDOM)

# This is the top-level output directory. Everything we generate goes in
# here.
OUT := out

# Needed for tcl files that are used with Cadence tools.
export dv_root := $(realpath ../../../vendor/lowrisc_ip/dv)
export DUT_TOP := dut

# Enable waveform dumping
WAVES               := 0
# Enable coverage dump
COV                 := 0
# Enable cosimulation flow
COSIM               := 1
# RTL simulator
SIMULATOR           := xlm
# ISS (spike, ovpsim)
ISS                 := spike
# Test name (default: full regression)
TEST                := all
TESTLIST            := $(EXT_DIR)/testlist.yaml
# Verbose logging
VERBOSE             :=
# Number of iterations for each test, assign a non-empty value to override the
# iteration count in the test list
ITERATIONS          :=
# Generator timeout limit in seconds
TIMEOUT             := 1800
# Pass/fail signature address at the end of test
SIGNATURE_ADDR      := 8ffffffc

### Ibex top level parameters ###
IBEX_CONFIG         := opentitan

# Derived directories from $(OUT), used for stuff that's built once or
# stuff that gets run for each seed, respectively. Using OUT-DIR on
# the way avoids ugly double slashes if $(OUT) happens to end in a /.
OUT-DIR   := $(dir $(OUT)/)
BUILD-DIR := $(OUT-DIR)build
RUN-DIR   := $(OUT-DIR)run

# This expands to '@' if VERBOSE is 0 or not set, and to the empty
# string otherwise. Prefix commands with it in order that they only
# get printed when VERBOSE.
verb = $(if $(filter-out 0,$(VERBOSE)),,@)

SHELL=/bin/bash

export PRJ_DIR        := $(realpath ../../..)
export LOWRISC_IP_DIR := $(realpath ${PRJ_DIR}/vendor/lowrisc_ip)

all: core_config sim

core_config:
	cp $(EXT_DIR)/riscv_core_setting.sv.default $(EXT_DIR)/riscv_core_setting.sv
	if [[ "$(IBEX_CONFIG)" == "small" ]] || [[ "$(IBEX_CONFIG)" == "experimental-branch-predictor" ]]; then \
		patch -u $(EXT_DIR)/riscv_core_setting.sv -i $(EXT_DIR)/riscv_core_setting.nopmp.sv.patch; \
	fi
	cd ..

instr: iss_sim

sim: post_compare $(if $(filter 1,$(COV)),merge_cov,)

.PHONY: clean
clean:
	rm -f $(EXT_DIR)/riscv_core_setting.sv
	rm -rf $(OUT-DIR)

# This is a list of directories that are automatically generated by some
# targets. To ensure the directory has been built, add a order-only dependency
# (with the pipe symbol before it) on the directory name and add the directory
# to this list.
gen-dirs := $(BUILD-DIR)

$(gen-dirs): %:
	mkdir -p $@

###############################################################################
# Utility functions.
#
# If VS is a list of variable names, P is a path and X is a string, then $(call
# dump-vars,P,X,VS) will expand to a list of 'file' commands that write each
# variable to P in Makefile syntax, but with "last-X-" prepended. At the start
# of the file, we also define last-X-vars-loaded to 1. You can use this to
# check whether there was a dump file at all.
#
# Note that this doesn't work by expanding to a command. Instead, *evaluating*
# dump-vars causes the variables to be dumped.
dump-var  = $(file >>$(1),last-$(2)-$(3) := $($(3)))
dump-vars = $(file >$(1),last-$(2)-vars-loaded := .) \
            $(foreach name,$(3),$(call dump-var,$(1),$(2),$(name)))

# equal checks whether two strings are equal, evaluating to '.' if they are and
# '' otherwise.
both-empty = $(if $(1),,$(if $(2),,.))
find-find = $(if $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))),.,)
equal = $(or $(call both-empty,$(1),$(2)),$(call find-find,$(1),$(2)))

# var-differs is used to check whether a variable has changed since it was
# dumped. If it has changed, the function evaluates to '.' (with some
# whitespace) and prints a message to the console; if not, it evaluates to ''.
#
# Call it as $(call var-differs,X,TGT,V).
var-differs = \
  $(if $(call equal,$(strip $($(3))),$(strip $(last-$(1)-$(3)))),,\
       .$(info Repeating $(2) because variable $(3) has changed value.))

# vars-differ is used to check whether several variables have the same value as
# they had when they were dumped. If we haven't loaded the dumpfile, it
# silently evaluates to '!'. Otherwise, if all the variables match, it
# evaluates to '.'. If not, it evaluates to '.' and prints some messages to the
# console explaining why a rebuild is happening.
#
# Call it as $(call vars-differ,X,TGT,VS).
vars-differ-lst = $(foreach v,$(3),$(call var-differs,$(1),$(2),$(v)))
vars-differ-sp = \
  $(if $(last-$(1)-vars-loaded),\
       $(if $(strip $(call vars-differ-lst,$(1),$(2),$(3))),.,),\
       !)
vars-differ = $(strip $(call vars-differ-sp,$(1),$(2),$(3)))

# A phony target which can be used to force recompilation.
.PHONY: FORCE
FORCE:

# vars-prereq is empty if every variable in VS matches the last run (loaded
# with tag X), otherwise it is set to FORCE (which will force a recompile and
# might print a message to the console explaining why we're rebuilding TGT).
#
# Call it as $(call vars-prereq,X,TGT,VS)
vars-prereq = $(if $(call vars-differ,$(call strip,$(1)),$(2),$(3)),FORCE,)

# Convert VERBOSE, COV, WAVE and COSIM to "store_true" arguments
verb-arg  := $(if $(filter-out 0,$(VERBOSE)),--verbose,)
cov-arg   := $(if $(filter 1,$(COV)),--en_cov,)
wave-arg  := $(if $(filter 1,$(WAVES)),--en_wave,)
cosim-arg := $(if $(filter 1,$(COSIM)),--en_cosim,)

###############################################################################
# Get a list of tests and seeds
#
# Run list_tests.py to list the things we need to run in the format
# TESTNAME.SEED and store it in a variable.
tests-and-seeds := \
  $(shell ./list_tests.py \
            --start_seed $(SEED) \
            --test "$(TEST)" \
            $(if $(ITERATIONS),--iterations $(ITERATIONS),) \
            --ibex-config $(IBEX_CONFIG))

# Define a variable that contains the output directories for all the
# test/seed combinations
ts-dirs := $(foreach ts,$(tests-and-seeds),$(RUN-DIR)/$(ts)/)

###############################################################################
###############################################################################
# Build the Random Instruction Generator
#
# This depends on the vendored in code in $(GEN_DIR). It also depends on the
# values of the following Makefile variables (we want to regenerate things if,
# for example, the simulator changes).
instr-gen-build-var-deps := SIMULATOR SIGNATURE_ADDR
# To achieve this variable tracking, we dump each of the variables to a Makefile
# fragment and try to load it up the next time around. This done with the
# utility function "dump-vars" at the end of the recipe.
#
# To create the dependency, we must do the following two things before each
# target:
#
# First, load up the saved variable values from the last time around. If this
# fails, it's no problem: we'll assume that the previous run either doesn't
# exist or something went wrong.
ig-build-vars-path := $(BUILD-DIR)/.instr-gen.vars.mk
-include $(ig-build-vars-path)

# Next, compare the current variables to those we just loaded. This uses the
# utility function "vars-prereq". It creates a variable which evaluates to the
# (phony) FORCE if the two sets of variables do not match.
#
# Note that we define it with '=', not ':=', so we don't evaluate if we're not
# trying to run the instr_gen_build target.
instr-gen-build-vars-prereq = \
  $(call vars-prereq, \
     gen, \
     building instruction generator, \
     $(instr-gen-build-var-deps))

# Finally, $(instr-gen-build-vars-prereq) becomes a dependency of our target.

riscv-dv-files := $(shell find $(GEN_DIR) -type f)
# A variable containing a file list for the riscv-dv vendored-in module.
# Depending on these files gives a safe over-approximation that will ensure we
# rebuild things if that module changes.
# Note that this is defined with ":=". As a result, we'll always run the find
# command exactly once. Wasteful if we're trying to make clean, but much better
# than running it for every target otherwise.

$(BUILD-DIR)/instr-gen/.compile.stamp: \
  $(instr-gen-build-vars-prereq) \
  $(riscv-dv-files) scripts/build-instr-gen.py | $(BUILD-DIR)
	$(verb)scripts/build-instr-gen.py        \
	  $(verb-arg)                            \
	  --simulator $(SIMULATOR)               \
	  --ibex-config $(IBEX_CONFIG)           \
	  --end-signature-addr $(SIGNATURE_ADDR) \
	  --output $(BUILD-DIR)/instr-gen
	$(call dump-vars,$(ig-build-vars-path),gen,$(instr-gen-build-var-deps))
	@touch $@

.PHONY: instr_gen_build
instr_gen_build: $(BUILD-DIR)/instr-gen/.compile.stamp

###############################################################################
# Run the random instruction generator
#
test-asms := $(addsuffix test.S,$(ts-dirs))

$(test-asms): \
  $(RUN-DIR)/%/test.S: \
  $(BUILD-DIR)/instr-gen/.compile.stamp \
  $(TESTLIST) \
  scripts/run-instr-gen.py
	$(verb)scripts/run-instr-gen.py          \
	  $(verb-arg)                            \
	  --simulator $(SIMULATOR)               \
	  --end-signature-addr $(SIGNATURE_ADDR) \
	  --output-dir $(@D)                     \
	  --gen-build-dir $(BUILD-DIR)/instr-gen \
	  --ibex-config $(IBEX_CONFIG)           \
	  --test-dot-seed $*

.PHONY: instr_gen_run
instr_gen_run: $(test-asms)

###############################################################################
# Compile the generated assembly programs
#
# We don't explicitly track dependencies on the RISCV toolchain, so this
# doesn't depend on anything more than the instr_gen stage did.
#
# Note that the compilation step generates a .o file and then uses
# objcopy to create a .bin. The ISS run uses the .o and the RTL run
# uses the .bin. In the Makefile, we just track the .bin to represent
# both.

test-bins := $(addsuffix test.bin,$(ts-dirs))

$(test-bins): \
  $(RUN-DIR)/%/test.bin: \
  $(RUN-DIR)/%/test.S scripts/compile-generated-test.py
	$(verb)scripts/compile-generated-test.py \
	  $(verb-arg)                            \
	  --input $(RUN-DIR)/$*/test.S           \
	  --output $@                            \
	  --ibex-config $(IBEX_CONFIG)           \
	  --test-dot-seed $*

.PHONY: instr_gen_compile
instr_gen_compile: $(test-bins)

###############################################################################
# Run the instruction set simulator
#
# This (obviously) depends on having compiled the generated programs, so we
# don't have to worry about variables that affect the 'gen' stage. The only
# other variable that's going to affect things is the actual choice of ISS. We
# cheat and include it in the path.

iss-sim-logs := $(addsuffix $(ISS).log,$(ts-dirs))

$(iss-sim-logs): \
  $(RUN-DIR)/%/$(ISS).log: \
  $(RUN-DIR)/%/test.bin scripts/run-iss.py
	$(verb)scripts/run-iss.py              \
	  $(verb-arg)                          \
	  --ibex-config $(IBEX_CONFIG)         \
	  --iss=$(ISS)                         \
	  --input=$(RUN-DIR)/$*/test.o         \
	  --output=$@

.PHONY: iss_run
iss_run: $(iss-sim-logs)


###############################################################################
# Compile ibex core TB
#
# Note that this doesn't depend on the seed: the DUT doesn't depend on which
# test we're running!
#
# It does, however, depend on various variables. These are listed in
# tb-compile-var-deps. See the 'gen' stage for more verbose explanations of how
# the variable dumping works.
#
# The compiled ibex testbench (obviously!) also depends on the design and the
# DV code. The clever way of doing this would be to look at a dependency
# listing generated by the simulator as a side-effect of doing the compile (a
# bit like using the -M flags with a C compiler). Unfortunately, that doesn't
# look like it's particularly easy, so we'll just depend on every .v, .sv or
# .svh file in the dv or rtl directories. Note that this variable is set with
# '=', rather than ':='. This means that we don't bother running the find
# commands unless we need the compiled testbench.
all-verilog = \
  $(shell find ../../../rtl -name '*.v' -o -name '*.sv' -o -name '*.svh') \
  $(shell find ../.. -name '*.v' -o -name '*.sv' -o -name '*.svh')

tb-compile-var-deps := SIMULATOR COV WAVES COSIM
tb-compile-vars-path := $(BUILD-DIR)/.tb.vars.mk
-include $(tb-compile-vars-path)
tb-compile-vars-prereq = $(call vars-prereq,comp,compiling TB,$(tb-compile-var-deps))

$(BUILD-DIR)/tb/.compile.stamp: \
  $(tb-compile-vars-prereq) $(all-verilog) $(risc-dv-files) \
  scripts/compile-tb.py yaml/rtl_simulation.yaml \
  | $(BUILD-DIR)
	$(verb)scripts/compile-tb.py             \
	  $(verb-arg)                            \
	  --ibex-config $(IBEX_CONFIG)           \
	  --output=$(BUILD-DIR)/tb               \
	  --shared-cov-dir=$(RUN-DIR)/shared_cov \
	  --simulator=$(SIMULATOR)               \
	  $(cov-arg) $(wave-arg) $(cosim-arg)
	$(call dump-vars,$(tb-compile-vars-path),comp,$(tb-compile-var-deps))
	@touch $@

.PHONY: rtl_tb_compile
rtl_tb_compile: $(BUILD-DIR)/tb/.compile.stamp

###############################################################################
# Run ibex RTL simulation with generated programs

rtl-sim-logs := $(addsuffix rtl.log,$(ts-dirs))

$(rtl-sim-logs): \
  $(RUN-DIR)/%/rtl.log:           \
  $(BUILD-DIR)/tb/.compile.stamp $(RUN-DIR)/%/test.bin scripts/run-rtl.py
	@echo Running RTL simulation at $@
	$(verb)scripts/run-rtl.py                \
	  --ibex-config $(IBEX_CONFIG)           \
	  --simulator $(SIMULATOR)               \
	  --shared-cov-dir=$(RUN-DIR)/shared_cov \
	  $(cov-arg) $(wave-arg)                 \
	  --signature-addr $(SIGNATURE_ADDR)     \
	  --test-dot-seed $*                     \
	  --binary $(RUN-DIR)/$*/test.bin        \
	  --rtl-sim-dir $(BUILD-DIR)/tb          \
	  --out-dir $(@D)

.PHONY: rtl_sim_run
rtl_sim_run: $(rtl-sim-logs)

###############################################################################
# Compare ISS and RTL sim results
#
# For a given TEST/SEED pair, the ISS and RTL logs appear at:
#
#   $(RUN-DIR)/$(TEST).$(SEED)/$(ISS).log
#   $(RUN-DIR)/$(TEST).$(SEED)/trace_core_00000000.log
#
# The comparison script compares these and writes to a result file at
#
#   $(RUN-DIR)/$(TEST).$(SEED)/test-result.yml

comp-results := $(addsuffix test-result.yml,$(ts-dirs))

$(comp-results): \
  $(RUN-DIR)/%/test-result.yml: \
  $(RUN-DIR)/%/rtl.log compare.py
	@echo Comparing traces for $*
	$(verb)./compare.py                         \
	  --test-dot-seed $*                        \
	  --iss $(ISS)                              \
	  --iss-trace $(@D)/$(ISS).log              \
	  --rtl-log $(@D)/rtl.log                   \
	  --rtl-trace $(@D)/trace_core_00000000.log \
	  $(cosim-arg) --cosim-trace $(@D)/spike_cosim.log \
	  --binary $(@D)/test.o                     \
	  --compare-log $(@D)/compare.log           \
	  --output $@

$(RUN-DIR)/regr.log: collect_results.py $(comp-results)
	@echo "Collecting up results (report at $@)"
	$(verb)./collect_results.py -o $(@D) $(comp-results)

.PHONY: post_compare
post_compare: $(RUN-DIR)/regr.log

###############################################################################
# Generate RISCV-DV functional coverage
# TODO(udi) - add B extension
$(RUN-DIR)/fcov/.fcov.stamp: $(comp-results)
	$(verb)python3 ${GEN_DIR}/cov.py \
	  --core ibex \
	  --dir $(RUN-DIR) \
	  -o $(RUN-DIR)/fcov \
	  --simulator $(SIMULATOR) \
	  --opts "--gen_timeout 1000" \
	  --isa rv32imcb \
	  --custom_target $(EXT_DIR)
	@ # Bookkeeping
	@touch $@

.PHONY: riscv_dv_fcov
riscv_dv_fcov: $(RUN-DIR)/fcov/.fcov.stamp

###############################################################################
# Merge all output coverage directories into the <out>/rtl_sim directory
#
# Any coverage databases generated from the riscv_dv_fcov target will be merged
# as well.
$(RUN-DIR)/coverage/.merge.stamp: \
  $(RUN-DIR)/fcov/.fcov.stamp \
  scripts/merge-cov.py
	$(verb)scripts/merge-cov.py    \
	  $(verb-arg)                  \
	  --working-dir=$(RUN-DIR)     \
	  --simulator=$(SIMULATOR)
	@ # Bookkeeping
	@touch $@

.PHONY: merge_cov
merge_cov: $(RUN-DIR)/coverage/.merge.stamp
